package cn.jetpiece.cloud.joss.app.modular.storage.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.jetpiece.cloud.core.util.HttpContext;
import cn.jetpiece.cloud.joss.api.object.model.enums.AclEnum;
import cn.jetpiece.cloud.joss.api.object.model.params.CompleteMultiPartUploadObject;
import cn.jetpiece.cloud.joss.api.object.model.params.InitMultiPartUploadObject;
import cn.jetpiece.cloud.joss.api.object.model.params.MultiPartUploadObject;
import cn.jetpiece.cloud.joss.api.object.model.result.JossPartEtag;
import cn.jetpiece.cloud.joss.api.object.model.result.UploadObjectResult;
import cn.jetpiece.cloud.joss.app.modular.basics.entity.StorageClass;
import cn.jetpiece.cloud.joss.app.modular.object.entity.Bucket;
import cn.jetpiece.cloud.joss.app.modular.object.entity.Provider;
import cn.jetpiece.cloud.joss.app.modular.object.entity.Region;
import cn.jetpiece.cloud.joss.app.modular.object.enums.BasicExceptionEnum;
import cn.jetpiece.cloud.joss.app.modular.object.model.BucketInfo;
import cn.jetpiece.cloud.joss.app.modular.storage.service.AbstractJossClient;
import cn.jetpiece.cloud.model.exception.ServiceException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.parser.Ranges;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLDecoder;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
public class OssJossClient extends AbstractJossClient {
    private final OSS client;

    public OssJossClient(String endpoint, String accessKey, String secretKey) {
        super(endpoint, accessKey, secretKey);
        client = new OSSClientBuilder().build(endpoint, accessKey, secretKey);
    }

    @Override
    public void createBucket(Provider provider, Region region, Bucket bucket, StorageClass storageClass) {
        try {
            CreateBucketRequest request = new CreateBucketRequest(bucket.getBucketName());
            request.setStorageClass(com.aliyun.oss.model.StorageClass.parse(storageClass.getStorageClassValue()));
            if (bucket.getAcl() == AclEnum.PRIVATE.getCode()) {
                request.setCannedACL(CannedAccessControlList.Private);
            } else if (bucket.getAcl() == AclEnum.PUBLIC_READ.getCode()) {
                request.setCannedACL(CannedAccessControlList.PublicRead);
            } else if (bucket.getAcl() == AclEnum.PUBLIC_READ_WRITE.getCode()) {
                request.setCannedACL(CannedAccessControlList.PublicReadWrite);
            }
            if (bucket.getDataRedundancyType() == 2) {
                request.setDataRedundancyType(DataRedundancyType.ZRS);
            }
            client.createBucket(request);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.BUCKET_CREATE_ERROR);
        }
    }

    @Override
    public void deleteBucket(BucketInfo bucketInfo) {
        try {
            client.deleteBucket(bucketInfo.getBucketName());
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_DELETE_ERROR);
        }
    }

    @Override
    public String initMultipartUpload(BucketInfo bucketInfo, String objectName, InitMultiPartUploadObject param) {
        try {
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketInfo.getBucketName(), objectName);
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentType(param.getContentType());
            if (param.getAcl() == AclEnum.PRIVATE.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.Private);
            } else if (param.getAcl() == AclEnum.PUBLIC_READ.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.PublicRead);
            } else if (param.getAcl() == AclEnum.PUBLIC_READ_WRITE.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.PublicReadWrite);
            }
            request.setObjectMetadata(objectMetadata);
            return client.initiateMultipartUpload(request).getUploadId();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }
    }

    @Override
    public JossPartEtag multipartUpload(BucketInfo bucketInfo, String objectName, MultiPartUploadObject param, MultipartFile file) {
        try {
            UploadPartRequest uploadPartRequest = new UploadPartRequest();
            uploadPartRequest.setBucketName(bucketInfo.getBucketName());
            uploadPartRequest.setKey(objectName);
            uploadPartRequest.setUploadId(param.getUploadId());
            uploadPartRequest.setInputStream(file.getInputStream());
            // 设置分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
            uploadPartRequest.setPartSize(param.getPartSize());
            // 设置分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出此范围，OSS将返回InvalidArgument错误码。
            uploadPartRequest.setPartNumber(param.getPartNumber());
            uploadPartRequest.setMd5Digest(param.getContentMd5());
            // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
            UploadPartResult uploadPartResult = client.uploadPart(uploadPartRequest);
            return BeanUtil.copyProperties(uploadPartResult.getPartETag(), JossPartEtag.class);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }
    }

    @Override
    public UploadObjectResult completeMultipartUpload(BucketInfo bucketInfo, String objectName, CompleteMultiPartUploadObject param) {
        try {
            List<PartETag> partEtag = param.getEtag().stream().map(item -> BeanUtil.copyProperties(item, PartETag.class)).collect(Collectors.toList());
            CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest(bucketInfo.getBucketName(), objectName, param.getUploadId(), partEtag);
            CompleteMultipartUploadResult completeMultipartUploadResult = client.completeMultipartUpload(request);
            OSSObject ossObject = client.getObject(bucketInfo.getBucketName(), objectName);
            UploadObjectResult result = new UploadObjectResult();
            result.setBucketName(bucketInfo.getBucketName());
            result.setContextType(ossObject.getObjectMetadata().getContentType());
            result.setObjectSize(ossObject.getObjectMetadata().getContentLength());
            result.setObjectName(objectName);
            result.setProviderUrl(URLDecoder.decode(completeMultipartUploadResult.getResponse().getUri(), "UTF-8"));
            return result;

        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }

    }

    @Override
    public String upload(BucketInfo bucketInfo, Integer acl, String objectName, File file) {
        if (acl == null) {
            acl = bucketInfo.getAcl();
        }
        try {
            PutObjectRequest request = new PutObjectRequest(bucketInfo.getBucketName(), objectName, file);
            ObjectMetadata objectMetadata = new ObjectMetadata();
            if (acl == AclEnum.PRIVATE.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.Private);
            } else if (acl == AclEnum.PUBLIC_READ.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.PublicRead);
            } else if (acl == AclEnum.PUBLIC_READ_WRITE.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.PublicReadWrite);
            }
            request.setMetadata(objectMetadata);
            PutObjectResult ossPutObjectResult = client.putObject(request);
            return URLDecoder.decode(ossPutObjectResult.getResponse().getUri(), "UTF-8");
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }
    }

    @Override
    public String upload(BucketInfo bucketInfo, Integer acl, String objectName, long objectSize, String contentType, byte[] objectBytes) {
        return upload(bucketInfo, acl, objectName, objectSize, contentType, new ByteArrayInputStream(objectBytes));
    }

    @Override
    public String upload(BucketInfo bucketInfo, Integer acl, String objectName, long objectSize, String contentType, InputStream inputStream) {
        if (acl == null) {
            acl = bucketInfo.getAcl();
        }
        try {
            PutObjectRequest request = new PutObjectRequest(bucketInfo.getBucketName(), objectName, inputStream);
            ObjectMetadata objectMetadata = new ObjectMetadata();
            if (acl == AclEnum.PRIVATE.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.Private);
            } else if (acl == AclEnum.PUBLIC_READ.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.PublicRead);
            } else if (acl == AclEnum.PUBLIC_READ_WRITE.getCode()) {
                objectMetadata.setObjectAcl(CannedAccessControlList.PublicReadWrite);
            }
            request.setMetadata(objectMetadata);
            PutObjectResult ossPutObjectResult = client.putObject(request);
            return URLDecoder.decode(ossPutObjectResult.getResponse().getUri(), "UTF-8");
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_UPLOAD_ERROR);
        }
    }

    @Override
    public void delete(BucketInfo bucketInfo, String objectName, boolean checkAcl) {
        try {
            if (checkAcl) {
                ObjectAcl accessControlList = client.getObjectAcl(bucketInfo.getBucketName(), objectName);
                if (accessControlList == null) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                } else if (accessControlList.getPermission() == ObjectPermission.Private) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                }
            }
            client.deleteObject(bucketInfo.getBucketName(), objectName);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_DELETE_ERROR);
        }
    }

    @Override
    public void getObject(BucketInfo bucketInfo, String objectName, boolean checkAcl) {
        HttpServletRequest request = HttpContext.getRequest();
        HttpServletResponse response = HttpContext.getResponse();
        try {
            if (checkAcl) {
                ObjectAcl accessControlList = client.getObjectAcl(bucketInfo.getBucketName(), objectName);
                if (accessControlList == null) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                } else if (accessControlList.getPermission() == ObjectPermission.Private) {
                    throw new ServiceException(BasicExceptionEnum.FORBIDDEN);
                }
            }
            Ranges ranges = getRanges(request);
            if (ranges == null) {
                GetObjectRequest getObjectRequest = new GetObjectRequest(bucketInfo.getBucketName(), objectName);
                String imageProcess = HttpContext.getRequestParameters().get("x-image-process");
                if (StrUtil.isNotBlank(imageProcess)) {
                    getObjectRequest.setProcess(imageProcess);
                }
                ObjectMetadata objectMetadata = client.getObjectMetadata(bucketInfo.getBucketName(), objectName);
                String ifNoneMatch = request.getHeader("If-None-Match");
                String ifModifiedSince = request.getHeader("If-Modified-Since");
                String etag = objectMetadata.getETag();
                String lastModified = objectMetadata.getLastModified().toString();
                response.setContentType(objectMetadata.getContentType());
                response.setHeader("ETag", etag);
                response.setHeader("Last-Modified", lastModified);
                if (StrUtil.isAllNotBlank(ifNoneMatch,ifModifiedSince) && etag.equals(ifNoneMatch) && ifModifiedSince.equals(lastModified)) {
                    response.setContentLengthLong(0L);
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                } else {
                    OSSObject ossObject = client.getObject(getObjectRequest);
                    response.setHeader("Accept-Ranges", "bytes");
                    response.setContentLengthLong(objectMetadata.getContentLength());
                    OutputStream outputStream = response.getOutputStream();
                    copyIO(ossObject.getObjectContent(), outputStream, objectMetadata.getContentLength());
                }
            } else {
                ObjectMetadata objectMetadata = client.getObjectMetadata(bucketInfo.getBucketName(), objectName);
                long startRange = ranges.getEntries().get(0).getStart();
                long endRange = ranges.getEntries().get(0).getEnd();
                if (endRange == -1) {
                    endRange = objectMetadata.getContentLength() - 1;
                }

                GetObjectRequest getObjectRequest = new GetObjectRequest(bucketInfo.getBucketName(), objectName);
                getObjectRequest.setRange(startRange, endRange);
                OSSObject ossObject = client.getObject(getObjectRequest);
                long contentLength = ossObject.getObjectMetadata().getContentLength();

                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                response.setContentType(ossObject.getObjectMetadata().getContentType());
                response.setHeader("Accept-Ranges", "bytes");
                response.setHeader("Content-Range", ranges.getUnits() + " " + startRange + "-" + endRange + "/" + objectMetadata.getContentLength());
                response.setHeader("ETag", ossObject.getObjectMetadata().getETag());
                response.setHeader("Last-Modified", ossObject.getObjectMetadata().getLastModified().toString());
                response.setContentLengthLong(ossObject.getObjectMetadata().getContentLength());
                OutputStream outputStream = response.getOutputStream();
                copyIO(ossObject.getObjectContent(), outputStream, ossObject.getObjectMetadata().getContentLength());
            }
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new ServiceException(BasicExceptionEnum.OBJECT_PREVIEW_ERROR);
        }
    }

    @Override
    public void destroy() {
        try {
            client.shutdown();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }

    }
}
